package com.xqboss.apps.service.punch;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xqboss.apps.constant.Config;
import com.xqboss.apps.convert.punch.PunchRuleConverter;
import com.xqboss.apps.convert.punch.PunchUserConfigConverter;
import com.xqboss.apps.domain.consumer.ConsumerTeamGainRule;
import com.xqboss.apps.domain.equipment.Equipment;
import com.xqboss.apps.domain.member.SysUserNft;
import com.xqboss.apps.domain.nft.NftNumber;
import com.xqboss.apps.domain.punch.*;
import com.xqboss.apps.domain.sys.Job;
import com.xqboss.apps.domain.user.UserIdentityRankSlot;
import com.xqboss.apps.dto.punch.EditUserPunchConfigDto;
import com.xqboss.apps.dto.punch.UserAddSlotDto;
import com.xqboss.apps.enums.mq.AppMqDelayEnum;
import com.xqboss.apps.enums.nft.NftTypeEnum;
import com.xqboss.apps.enums.nft.UserNftStatusEnum;
import com.xqboss.apps.enums.sys.JobConfigStateEnum;
import com.xqboss.apps.mapper.punch.PunchUserConfigMapper;
import com.xqboss.apps.service.consumer.ConsumerTeamGainRuleService;
import com.xqboss.apps.service.consumer.ConsumerTeamService;
import com.xqboss.apps.service.equipment.EquipmentService;
import com.xqboss.apps.service.nft.NftNumberService;
import com.xqboss.apps.service.sys.JobService;
import com.xqboss.apps.service.user.SysUserNftService;
import com.xqboss.apps.service.user.UserIdentityRankService;
import com.xqboss.apps.util.mh.BeanCopierUtil;
import com.xqboss.apps.vo.consumer.ConsumerPunchTeamVo;
import com.xqboss.apps.vo.consumer.ConsumerTeamVo;
import com.xqboss.apps.vo.consumer.TeamGainInfoVo;
import com.xqboss.apps.vo.equipment.EquipmentInfoVo;
import com.xqboss.apps.vo.punch.*;
import com.xqboss.apps.vo.user.UserNFTItemVo;
import com.xqboss.common.core.mq.MqSender;
import com.xqboss.common.core.redis.lock.LockValue;
import com.xqboss.common.core.redis.lock.RedisLock;
import com.xqboss.common.exception.ServiceException;
import com.xqboss.system.service.ISysConfigService;
import org.apache.commons.lang3.BooleanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.stream.Collectors;

/**
 * <p>
 * 用户打卡配置服务
 * </p>
 *
 * @author Trent
 * @date 2023/8/15
 */
@Service
public class PunchUserConfigService extends ServiceImpl<PunchUserConfigMapper, PunchUserConfig> {

    @Autowired
    private PunchRuleService punchRuleService;

    @Autowired
    private SysUserNftService userNftService;

    @Autowired
    private NftNumberService nftNumberService;

    @Autowired
    private EquipmentService equipmentService;

    @Autowired
    private ISysConfigService sysConfigService;

    @Lazy
    @Autowired
    private MqSender mqSender;

    @Autowired
    private PunchUserConfigConverter punchUserConfigConverter;

    @Autowired
    private PunchRuleConverter punchRuleConverter;

    @Autowired
    private UserPunchService userPunchService;

    @Autowired
    private PunchWeakenConfigService punchWeakenConfigService;

    @Autowired
    private ConsumerTeamService consumerTeamService;

    @Autowired
    SysUserNftService sysUserNftService;

    @Autowired
    private ConsumerTeamGainRuleService teamGainRuleService;

    @Autowired
    private PunchUserRecordService punchUserRecordService;

    @Autowired
    private UserIdentityRankService userIdentityRankService;

    @Autowired
    private PunchUserSlotService punchUserSlotService;
    @Autowired
    private JobService jobService;


    /**
     * 获取用户打卡配置
     *
     * @return
     */
    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
    @RedisLock(lockKey = "UserPunchConfig")
    public PunchUserConfig getConfig(@LockValue Long userId) {
        PunchUserConfig config = getOne(new LambdaQueryWrapper<PunchUserConfig>()
                .eq(PunchUserConfig::getUserId, userId));
        if (config == null) {
            config = new PunchUserConfig();
            config.setUserId(userId);
            // 打卡规则id暂时写死 只有一个规则
            config.setPunchRuleId(1L);
            save(config);
        }
        // 获取用户身份等级应该拥有的卡槽
        List<UserIdentityRankSlot> slotList = Optional.ofNullable(userIdentityRankService.getSlotByUserId(userId)).orElse(Collections.emptyList());
        // 获取用户已经拥有的卡槽
        List<PunchUserSlot> userSlotList = punchUserSlotService.list(new LambdaQueryWrapper<PunchUserSlot>()
                .eq(PunchUserSlot::getUserId, userId));
        // 根据userId与卡槽id比较用户拥有的卡槽和应该拥有的卡槽，如果不一致则新增卡槽并且根据身份卡槽slotList的type类型expiration有效期来设置新增卡槽的status状态
        // 将用户已经拥有的卡槽的ID转换为一个set集合
        Set<Long> userSlotIdSet = Optional.ofNullable(userSlotList)
                .orElse(Collections.emptyList())
                .stream()
                .map(PunchUserSlot::getSlotId)
                .collect(Collectors.toSet());

        // 过滤出用户尚未拥有的卡槽，并将其映射为新的卡槽对象
        List<PunchUserSlot> slotsToAdd = slotList.stream()
                .filter(slot -> !userSlotIdSet.contains(slot.getId()))
                .map(slot -> {
                    PunchUserSlot newSlot = new PunchUserSlot();
                    newSlot.setUserId(userId);
                    newSlot.setSlotId(slot.getId());
                    // 如果卡槽有效期不为空，设置用户卡槽过期时间
                    if (slot.getExpiration() != null && slot.getExpiration() > 0) {
                        newSlot.setExpireTime(LocalDateTime.now().plusDays(slot.getExpiration()));
                    }
                    // 如果卡槽为续费卡槽设置状态为 未解锁，免费卡槽设置状态为 未使用
                    if (slot.getType() == 1) {
                        newSlot.setStatus(-1);
                    } else {
                        newSlot.setStatus(0);
                    }
                    return newSlot;
                })
                .collect(Collectors.toList());
        // 如果有需要添加的卡槽，将它们保存到数据库
        if (!slotsToAdd.isEmpty()) {
            punchUserSlotService.saveBatch(slotsToAdd);
        }
        return config;
    }

    /**
     * 获取用户打卡配置
     *
     * @param userId
     * @return
     */
    public UserPunchConfigVo getPunchConfigVo(Long userId) {
        PunchUserConfig punchUser = getConfig(userId);
        UserPunchConfigVo vo = punchUserConfigConverter.toVo(punchUser);

        if (punchUser.getPunchRuleId() != null) {
            // 打卡规则
            PunchRule rule = punchRuleService.getById(punchUser.getPunchRuleId());
            if (rule != null && BooleanUtils.isTrue(rule.getSaleable())) {
                vo.setPunchRule(punchRuleConverter.toVo(rule));
                vo.setPunchInterval(rule.getIntervalTime());
                LocalDateTime nextPunchTime = vo.getNextPunchTime();
                LocalTime nowTime = LocalTime.now();
                //如果下次打卡时间是未来时间，则不用处理，如果下次打卡时间是之前的时间，有可能已经过期了， 需要重新计算
                if (nextPunchTime == null || nextPunchTime.isBefore(LocalDateTime.now())) {
                    // 如果计算的下次打卡时段是今天之前的时间
                    if (nowTime.isAfter(rule.getEndTime())) {
                        nextPunchTime = LocalDateTime.of(LocalDate.now().plusDays(1), rule.getStartTime());
                    } else {
                        nextPunchTime = LocalDateTime.of(LocalDate.now(), rule.getStartTime());
                    }
                }
                vo.setNextPunchTime(nextPunchTime);

                long countdown = LocalDateTimeUtil.between(LocalDateTime.now(), nextPunchTime, ChronoUnit.MILLIS);
                if (countdown < 0) {
                    countdown = 0;
                }
                vo.setPunchCountdown((int) countdown);

                // 只有有打卡规则才检测用户装备信息
                if (punchUser.getUserNftId() != null) {
                    UserNFTItemVo equipmentNftInfo = sysUserNftService.getNftItemInfo(punchUser.getUserNftId(), userId);
                    // 判断装备是否在打卡时段 ，当用户今日没有打卡记录，则也算不在打卡时段
                    if(vo.getTodayPunchCount() > 0){
                        equipmentNftInfo.setIsPunchTime(!(nowTime.isBefore(rule.getStartTime()) || nowTime.isAfter(rule.getEndTime())));
                    }
                    vo.setEquipmentInfo(equipmentNftInfo);
                }
            }
        }

        // 计算下次打卡收益
        if (Objects.nonNull(vo.getEquipmentInfo())) {
            // 计算正常收益
            BigDecimal income = userPunchService.calcPunchIncome(vo.getEquipmentInfo().getEquipment().getId());
            BigDecimal dayIncomeMax = sysConfigService.getValue(Config.Sys.Punch.日打卡收益上限);
            if (Objects.nonNull(dayIncomeMax)) {
                // 如果设置了打卡上限
                if (LocalDateTimeUtil.isSameDay(vo.getNextPunchTime(), LocalDateTime.now())) {
                    // 如果下次打卡是当天，则判断今日打卡是否达到上限
                    BigDecimal subIncome = dayIncomeMax.subtract(vo.getTodayPunchIncome());
                    if (subIncome.compareTo(BigDecimal.ZERO) <= 0) {
                        income = BigDecimal.ZERO;
                    } else {
                        // 如果剩余的收益额度小于打卡收益额度，则以剩余收益额度为准
                        if (subIncome.compareTo(income) < 0) {
                            income = subIncome;
                        }
                    }
                } else {
                    // 如果不是同一天，则判断打卡收益是否超过打卡上限
                    if (dayIncomeMax.compareTo(income) < 0) {
                        income = dayIncomeMax;
                    }
                }
            }
            EquipmentInfoVo equipment = vo.getEquipmentInfo().getEquipment();
            BigDecimal durabilityRate = equipment.getDurability().divide(equipment.getTotalDurability(), 2, RoundingMode.UP);
            PunchWeakenConfig punchWeakenConfig = punchWeakenConfigService.getPunchWeakenConfig(durabilityRate);
            vo.setNextPunchWeaken(punchWeakenConfig);
            vo.setNextPunchIncome(income);
            vo.setTodayCanPunch(LocalDateTimeUtil.isSameDay(vo.getNextPunchTime(), LocalDateTime.now()));
        }

        // 获取队伍信息
        ConsumerTeamVo myTeam = consumerTeamService.getMyTeam(userId);
        ConsumerPunchTeamVo teamVo = BeanCopierUtil.copy(myTeam, ConsumerPunchTeamVo.class);
        vo.setTeamInfo(teamVo);
        if(Objects.nonNull(teamVo)){
            // 获取当前队伍的打卡收益信息
            BigDecimal teamTodayIncome = punchUserRecordService.getUserTotalIncome(userId, myTeam.getId(), LocalDate.now());
            ConsumerTeamGainRule currentGain = teamGainRuleService.getPunchCountRule(teamVo.getTotalPunch(),myTeam.getRank());
            if(currentGain != null){
                TeamGainInfoVo gainVo = new TeamGainInfoVo();
                gainVo.setGain(currentGain.getGain());
                gainVo.setLackPunchCount(0);
                gainVo.setPunchCount(currentGain.getTotalPunch());
                gainVo.setIncome(teamTodayIncome.multiply(gainVo.getGain()).setScale(2, RoundingMode.UP));
                teamVo.setCurrentGainInfo(gainVo);
            }

            // v1.6.0：打卡页更改。由于打卡页数据已写死，取的下阶段的增益百分比。现把下阶段的收益百分比改为当前阶段的。若没有达到最低打卡次数满足的百分比，就返回百分之零。
            ConsumerTeamGainRule nextGain = teamGainRuleService.getNextPunchCountRule(teamVo.getTotalPunch(),myTeam.getRank());
            if(nextGain != null){
                /*//计算下次预估时，当用户今日队伍没有收益时，以下次预估收益为准计算下阶段数据
                if(teamTodayIncome.compareTo(BigDecimal.ZERO) <= 0){
                    teamTodayIncome = vo.getNextPunchIncome();
                }
                TeamGainInfoVo gainVo = new TeamGainInfoVo();
                gainVo.setGain(nextGain.getGain());
                gainVo.setPunchCount(nextGain.getTotalPunch());
                gainVo.setLackPunchCount(gainVo.getPunchCount() - teamVo.getTotalPunch());
                if(teamTodayIncome != null){
                    gainVo.setIncome(teamTodayIncome.multiply(gainVo.getGain()).setScale(2, RoundingMode.UP));
                }
                teamVo.setNextGainInfo(gainVo);*/


                TeamGainInfoVo gainVo = new TeamGainInfoVo();
                gainVo.setGain(currentGain != null?currentGain.getGain():BigDecimal.ZERO);
                gainVo.setLackPunchCount(0);
                gainVo.setPunchCount(currentGain != null?currentGain.getTotalPunch():0);
                gainVo.setIncome(teamTodayIncome.multiply(gainVo.getGain()).setScale(2, RoundingMode.UP));
                teamVo.setNextGainInfo(gainVo);
            }
        }
        return vo;
    }

    /**
     * 更新打卡配置
     */
    @Transactional(rollbackFor = Exception.class)
    public void updateConfig(Long userId, Long ruleId, Long userNftId) {
        PunchUserConfig config = getConfig(userId);
        if (Objects.nonNull(config.getPunchRuleId())) {
            // 判断今日是否已经打过卡
            if(config.getTodayPunchCount() > 0){
                PunchRule oldRule = punchRuleService.getById(config.getPunchRuleId());
                if (Objects.nonNull(oldRule) && BooleanUtils.isTrue(oldRule.getSaleable())) {
                    // 如果在打卡时段内，则不能修改
                    LocalTime now = LocalTime.now();
                    if (now.isAfter(oldRule.getStartTime()) && now.isBefore(oldRule.getEndTime())) {
                        if (Objects.nonNull(userNftId)) {
                            if (!Objects.equals(config.getPunchRuleId(), ruleId)) {
                                //设置用户一天只能选择一个时间段，到了第一个时间段也需要第二天选择另外时间段
                                if (config.getPunchRuleId() != null && config.getTodayPunchCount() > 0) {
                                    throw new ServiceException("您今天已经打过卡了，请次日调整时间段");
                                }
                                throw new ServiceException("打卡中不能调整时间~");
                            }
                            // 如果当前打卡装备破损度或者耐久度不够打卡一次消耗时(//如果当前用户身份高等级且时间段内未达到总打卡次数，支持切换装备)，支持切换装备
                            if (!Objects.equals(config.getUserNftId(), userNftId)) {
                                Equipment equipment = equipmentService.getByUserNftId(config.getUserNftId(), userId);
                                //获取用户等级对应总打卡次数
                                Integer rankCount = userIdentityRankService.getUserRankIdentityPunchCount(userId, oldRule.getPunchCount());
                                //该装备时间段打卡次数
                                long nftPunch = punchUserRecordService.countUserRuleNftNumberIdPunch(userId, ruleId, equipment.getNftNumberId(), LocalDate.now());
                                if((config.getTodayPunchCount().compareTo(rankCount) >= 0 //如果当前时间段打卡总数 >= 等级总打卡数，则不能切换装备
                                        || (nftPunch != (long)oldRule.getPunchCount() && nftPunch > 0)) //或者当前装备时间段打卡过一次 并且没有达到时间段总打卡次数，则不能切换装备
                                        && (equipment.getBreakage().compareTo(sysConfigService.getValue(Config.Sys.Punch.消耗破损值)) > 0
                                        && equipment.getDurability().compareTo(userPunchService.calcDurabilityUsd(equipment)) > 0)){
                                    if (!Objects.equals(config.getUserNftId(), userNftId)) {
                                        throw new ServiceException("打卡中不能调整装备~");
                                    }
                                }
                            }
                        }else{
                            // 卸载装备
                            Equipment equipment = equipmentService.getByUserNftId(config.getUserNftId(), userId);
                            if(equipment.getBreakage().compareTo(sysConfigService.getValue(Config.Sys.Punch.消耗破损值)) > 0
                                    && equipment.getDurability().compareTo(userPunchService.calcDurabilityUsd(equipment)) > 0){
                                if (!Objects.equals(config.getUserNftId(), userNftId)) {
                                    throw new ServiceException("打卡中不能调整装备~");
                                }
                            }
                        }
                    }
                }
            }
        }
        LambdaUpdateWrapper<PunchUserConfig> updateWrapper;
        if (Objects.isNull(userNftId)) {
            // 卸载装备
            // 更新旧装备状态
            if (Objects.nonNull(config.getUserNftId())) {
                userNftService.updateStatus(config.getUserNftId(), UserNftStatusEnum.NORMAL);
            }
            // 更新打卡规则、打卡装备
            updateWrapper = new LambdaUpdateWrapper<PunchUserConfig>()
                    .set(PunchUserConfig::getPunchRuleId, null)
                    .set(PunchUserConfig::getNftNumberId, null)
                    .set(PunchUserConfig::getUserNftId, null)
                    .set(PunchUserConfig::getNextPunchTime, null)
                    .eq(PunchUserConfig::getId, config.getId());
        } else {
            //1. 判断规则是否有效
            PunchRule rule = punchRuleService.requireSaleableRule(ruleId);

            //2. 判断持有者是否是自己，
            SysUserNft userNft = userNftService.getById(userNftId);
            if (userNft == null) {
                throw new ServiceException("装备不存在！");
            }
            if (!Objects.equals(userId, userNft.getUserId())) {
                throw new ServiceException("装备不存在！");
            }
            if (!Objects.equals(userNftId, config.getUserNftId())) {
                // 更换装备， 判断新装备状态
                //3. 判断当前NFT状态是否正常，售卖中、装备中等不可用
                if (userNft.getNftStatus() != UserNftStatusEnum.NORMAL) {
                    throw new ServiceException("该装备暂不可用");
                }
                // 更新装备状态：将旧装备释放， 新装备修改为装备中
                if (Objects.nonNull(config.getUserNftId())) {
                    userNftService.updateStatus(config.getUserNftId(), UserNftStatusEnum.NORMAL);
                }
                userNftService.updateStatus(userNftId, UserNftStatusEnum.USED);
            }
            NftNumber nftNumber = nftNumberService.getById(userNft.getNftNumberId());

            //4. 更新打卡配置
            updateWrapper = new LambdaUpdateWrapper<PunchUserConfig>()
                    .set(PunchUserConfig::getNftNumberId, nftNumber.getId())
                    .set(PunchUserConfig::getUserNftId, userNft.getId())
                    .eq(PunchUserConfig::getId, config.getId());

            if (!Objects.equals(config.getPunchRuleId(), ruleId)) {
                //设置用户一天只能选择一个时间段，到了第一个时间段也需要第二天选择另外时间段
                if (config.getPunchRuleId() != null && config.getTodayPunchCount() > 0) {
                    throw new ServiceException("您今天已经打过卡了，请次日调整时间段");
                }
                // 如果修改了打卡时段，则修改打卡时间
                LocalTime now = LocalTime.now();
                LocalDateTime punchTime;
                mqSender.cancelDelay(AppMqDelayEnum.PUNCH_COUNTDOWN, config.getId());
                // 获取今日打卡次数，如果已经达到上限则下次打卡也是明天
                if (now.isBefore(rule.getEndTime()) && config.getTodayPunchCount() < rule.getPunchCount()) {
                    punchTime = LocalDateTime.of(LocalDate.now(), rule.getStartTime());
                } else {
                    punchTime = LocalDateTime.of(LocalDate.now().plusDays(1), rule.getStartTime());
                    mqSender.sendDelay(AppMqDelayEnum.PUNCH_COUNTDOWN, config.getId(), punchTime.plusMinutes(-1));
                }
                updateWrapper.set(PunchUserConfig::getPunchRuleId, ruleId);
                updateWrapper.set(PunchUserConfig::getNextPunchTime, punchTime);
            }
        }


        boolean update = update(updateWrapper);
        if (!update) {
            throw new ServiceException("操作失败");
        }
    }

    /**
     * 预估打卡收益
     *
     * @param ruleId 规则ID
     * @return
     */
    public BigDecimal estimatePunchIncome(Long userId, Long ruleId, Long userNftId) {
        //1. 判断规则是否有效
        PunchRule rule = punchRuleService.requireSaleableRule(ruleId);
        //2. 判断持有者是否是自己，
        SysUserNft userNft = userNftService.getById(userNftId);
        if (userNft == null || userNft.getNftType() != NftTypeEnum.EQUIPMENT) {
            throw new ServiceException("装备不存在！");
        }
        if (!Objects.equals(userId, userNft.getUserId())) {
            throw new ServiceException("装备不存在！");
        }

        // 打卡收益 = 幸运值 * 收益折算系数 * 耐久度削弱比列
        Equipment equipment = equipmentService.getByNftNumberId(userNft.getNftNumberId());
        if (Objects.isNull(equipment)) {
            throw new ServiceException("装备不存在！");
        }
        //预估打卡收益 = 单次打卡收益 * 打卡次数
        BigDecimal incomeRatio = Optional.ofNullable(equipment.getIncomeRatio())
                .orElse(sysConfigService.getValue(Config.Sys.Punch.收益折算系数));
        return incomeRatio
                .multiply(BigDecimal.valueOf(equipment.getLuck()))
                .multiply(BigDecimal.valueOf(rule.getPunchCount()))
                .setScale(2, RoundingMode.UP);
    }

    /**
     * 根据打卡记录更新打卡配置信息
     *
     * @param rule
     * @param record
     */
    @Transactional(rollbackFor = Exception.class)
    public void updateConfigByRecord(PunchUserConfig config, PunchRule rule, PunchUserRecord record) {
        // 计算下次打卡时间
        LocalDateTime now = LocalDateTime.now();
        // 判断是否已经达到最大打卡次数限制
        long punchCount = punchUserRecordService.countUserRulePunch(record.getUserId(), rule.getId(), LocalDate.now());
        LocalDateTime nextTime;
        if(punchCount >= rule.getPunchCount()){
            // 已经达到最大打卡次数限制
            // 下次打卡时间为明天的开始时间
            nextTime = LocalDateTime.of(LocalDate.now().plusDays(1), rule.getStartTime());
        }else {
            nextTime = now.plusSeconds(rule.getIntervalTime());
            if (nextTime.isAfter(LocalDateTime.of(LocalDate.now(), rule.getEndTime()))) {
                // 下次打卡在打卡时段之后，则下次打卡为明天的开始时间
                nextTime = LocalDateTime.of(LocalDate.now().plusDays(1), rule.getStartTime());
            }
        }

        config.setNextPunchTime(nextTime);
        boolean update = update(new LambdaUpdateWrapper<PunchUserConfig>()
                .set(PunchUserConfig::getLastPunchTime, now)
                .set(PunchUserConfig::getNextPunchTime, nextTime)
                .setSql("today_punch_count = today_punch_count + 1")
                .setSql("total_punch_count = total_punch_count + 1")
                .setSql("today_punch_income = today_punch_income + " + record.getIncome())
                .setSql("total_punch_income = total_punch_income + " + record.getIncome())
                .eq(PunchUserConfig::getId, config.getId()));
        if (!update) {
            throw new ServiceException("系统繁忙");
        }
    }

    /**
     * 更新打卡统计信息
     * @param config
     * @param rule
     * @param todayPunchCount
     * @param totalIncome
     */
    @Transactional(rollbackFor = Exception.class)
    public void updateConfig(PunchUserConfig config, PunchRule rule, int todayPunchCount, BigDecimal totalIncome) {
        // 计算下次打卡时间
        LocalDateTime now = LocalDateTime.now();
        LocalDateTime nextTime;
        // 判断是否已经达到最大打卡次数限制
        if(todayPunchCount >= rule.getPunchCount()){
            // 已经达到最大打卡次数限制
            // 下次打卡时间为明天的开始时间
            nextTime = LocalDateTime.of(LocalDate.now().plusDays(1), rule.getStartTime());
        }else {
            nextTime = now.plusSeconds(rule.getIntervalTime());
            if (nextTime.isAfter(LocalDateTime.of(LocalDate.now(), rule.getEndTime()))) {
                // 下次打卡在打卡时段之后，则下次打卡为明天的开始时间
                nextTime = LocalDateTime.of(LocalDate.now().plusDays(1), rule.getStartTime());
            }
        }

        config.setNextPunchTime(nextTime);
        boolean update = update(new LambdaUpdateWrapper<PunchUserConfig>()
                .set(PunchUserConfig::getLastPunchTime, now)
                .set(PunchUserConfig::getNextPunchTime, nextTime)
                .setSql("today_punch_count = today_punch_count + 1")
                .setSql("total_punch_count = total_punch_count + 1")
                .setSql("today_punch_income = today_punch_income + " + totalIncome)
                .setSql("total_punch_income = total_punch_income + " + totalIncome)
                .eq(PunchUserConfig::getId, config.getId()));
        if (!update) {
            throw new ServiceException("系统繁忙");
        }
    }


    /**
     * 清除今日打卡数据
     */
    public void cleanTodayData() {
        baseMapper.cleanTodayData();
        baseMapper.slotCleanTodayData();
    }

    /**
     * 是否处于打卡时段
     *
     * @return
     */
    @Transactional(rollbackFor = Exception.class)
    public boolean isPunchTime(Long userId) {
        PunchUserConfig config = getConfig(userId);
        // 用户今日未打卡则不算打卡时段
        if (config.getTodayPunchCount() > 0 && Objects.nonNull(config.getPunchRuleId())) {
            PunchRule rule = punchRuleService.getById(config.getPunchRuleId());
            if (Objects.nonNull(rule) && BooleanUtils.isTrue(rule.getSaleable())) {
                // 如果在打卡时段内，则不能修改
                LocalTime now = LocalTime.now();
                if (!(now.isBefore(rule.getStartTime()) || now.isAfter(rule.getEndTime()))) {
                    return true;
                }
            }
        }
        return false;
    }


    /**
     * 清除用户打卡规则
     *
     * @param punchRuleId
     */
    @Transactional(rollbackFor = Exception.class)
    public void cleanPunchRule(Long punchRuleId) {
        // 清除用户打卡装备状态
        sysUserNftService.cleanPunchRule(punchRuleId);
        // 下架用户打卡规则
        update(new LambdaUpdateWrapper<PunchUserConfig>()
//                .set(PunchUserConfig::getPunchRuleId, null)
                .set(PunchUserConfig::getUserNftId, null)
                .set(PunchUserConfig::getNftNumberId, null)
                .eq(PunchUserConfig::getPunchRuleId, punchRuleId));
    }

    /**
     * 用户身份升级成功后修改用户时间段可打卡总次数
     */
    public void upgradeEditPunchCount(Long userId, PunchUserConfig config, PunchRule rule) {
        // 计算下次打卡时间
        LocalDateTime now = LocalDateTime.now();
        // 判断是否已经达到最大打卡次数限制
        long punchCount = punchUserRecordService.countUserRulePunch(userId, rule.getId(), LocalDate.now());
        LocalDateTime nextTime;
        if (punchCount >= rule.getPunchCount()) {
            // 已经达到最大打卡次数限制
            // 下次打卡时间为明天的开始时间
            nextTime = LocalDateTime.of(LocalDate.now().plusDays(1), rule.getStartTime());
        } else {
            nextTime = now.plusSeconds(rule.getIntervalTime());
            if (nextTime.isAfter(LocalDateTime.of(LocalDate.now(), rule.getEndTime()))) {
                // 下次打卡在打卡时段之后，则下次打卡为明天的开始时间
                nextTime = LocalDateTime.of(LocalDate.now().plusDays(1), rule.getStartTime());
            }
        }

        config.setNextPunchTime(nextTime);
        boolean update = update(new LambdaUpdateWrapper<PunchUserConfig>()
                .set(PunchUserConfig::getLastPunchTime, now)
                .set(PunchUserConfig::getNextPunchTime, nextTime)
//                .setSql("today_punch_count = today_punch_count + 1")
//                .setSql("total_punch_count = total_punch_count + 1")
//                .setSql("today_punch_income = today_punch_income + " + record.getIncome())
//                .setSql("total_punch_income = total_punch_income + " + record.getIncome())
                .eq(PunchUserConfig::getId, config.getId()));
        if (!update) {
            throw new ServiceException("系统繁忙");
        }
    }


    /**
     * 定时任务：开启打卡规则
     */
    public boolean openPunchRule() {
        // 获取所有打卡规则
        List<PunchRule> list = punchRuleService.list(new LambdaQueryWrapper<PunchRule>()
                .eq(PunchRule::getSaleable, false));
        //如果不为空，则开启打卡规则
        if (CollUtil.isNotEmpty(list)) {
            list.forEach(rule -> {
                //更新打卡开始时间至结束时间超过12小时的打卡规则
                if (rule.getStartTime().until(rule.getEndTime(), ChronoUnit.HOURS) >= 12) {
                    // 更新打卡规则为上架状态
                    punchRuleService.changeSaleable(rule.getId(), true);
                }
            });
            return true;
        }
        return false;
    }


    /**
     * 计算打卡日并设置过期时间
     * 每天01：00执行，检查今日是否打卡日（过期时间大于当天00.00）
     * 如果不是打卡日，那么给所有用户有过期时间的卡槽+1天
     */
    public void calculatePunchSetExpiration() {
        // 获取任务，校验今日是否可以打卡
        Job job = jobService.isAirdrop(JobConfigStateEnum.PUNCH, LocalDateTime.now());
        if (Objects.nonNull(job)) {
            // 如果job不为空表示不打卡日, 执行关闭打卡规则逻辑
            boolean b = closePunchRule();
            if (b) {
                // 获取所有用户有过期时间并且过期时间大于今天00.00的卡槽
                List<PunchUserSlot> userSlotList = punchUserSlotService.list(new LambdaQueryWrapper<PunchUserSlot>()
                        .select(PunchUserSlot::getId, PunchUserSlot::getExpireTime)
                        .isNotNull(PunchUserSlot::getExpireTime)
                        .gt(PunchUserSlot::getExpireTime, LocalDate.now().atStartOfDay()));
                // 如果userSlotList不为空给所有用户有过期时间的卡槽+1天
                if (CollUtil.isNotEmpty(userSlotList)) {
                    userSlotList.forEach(slot -> {
                        slot.setExpireTime(slot.getExpireTime().plusDays(1));
                    });
                    punchUserSlotService.updateBatchById(userSlotList);
                }
            }
        } else {
            // 为空表示打卡日，执行开启打卡逻辑
            openPunchRule();
        }
    }

    /**
     * 定时任务：关闭打卡规则
     */
    public boolean closePunchRule() {
        // 获取所有打卡规则
        List<PunchRule> list = punchRuleService.list(new LambdaQueryWrapper<PunchRule>()
                .eq(PunchRule::getSaleable, true));
        //如果不为空，则关闭打卡规则
        if (CollUtil.isNotEmpty(list)) {
            list.forEach(rule -> {
                //更新打卡开始时间至结束时间超过12小时的打卡规则
                if (rule.getStartTime().until(rule.getEndTime(), ChronoUnit.HOURS) >= 12) {
                    // 更新打卡规则为上架状态
                    punchRuleService.changeSaleable(rule.getId(), false);
                }
            });
            return true;
        }
        return false;
    }

    /**
     * 获取用户打卡首页信息
     * @param userId
     * @return
     */
    public UserPunchIndexVo getPunchIndex(Long userId) {
        PunchUserConfig punchUser = getConfig(userId);
        UserPunchIndexVo vo = BeanUtil.copyProperties(punchUser, UserPunchIndexVo.class);

        if (punchUser.getPunchRuleId() != null) {
            // 打卡规则
            PunchRule rule = punchRuleService.getById(punchUser.getPunchRuleId());
            if (rule != null && BooleanUtils.isTrue(rule.getSaleable())) {
                vo.setPunchInterval(rule.getIntervalTime());
                vo.setPunchStartTime(rule.getStartTime());
                vo.setPunchEndTime(rule.getEndTime());
                vo.setPunchRule(BeanUtil.copyProperties(rule, PunchRuleVo.class));
                LocalDateTime nextPunchTime = vo.getNextPunchTime();
                LocalTime nowTime = LocalTime.now();
                //如果下次打卡时间是未来时间，则不用处理，如果下次打卡时间是之前的时间，有可能已经过期了， 需要重新计算
                if (nextPunchTime == null || nextPunchTime.isBefore(LocalDateTime.now())) {
                    // 如果计算的下次打卡时段是今天之前的时间
                    if (nowTime.isAfter(rule.getEndTime())) {
                        nextPunchTime = LocalDateTime.of(LocalDate.now().plusDays(1), rule.getStartTime());
                    } else {
                        nextPunchTime = LocalDateTime.of(LocalDate.now(), rule.getStartTime());
                    }
                }
                vo.setNextPunchTime(nextPunchTime);

                long countdown = LocalDateTimeUtil.between(LocalDateTime.now(), nextPunchTime, ChronoUnit.MILLIS);
                if (countdown < 0) {
                    countdown = 0;
                }
                vo.setPunchCountdown((int) countdown);
                // 耐久消耗系数 破损消耗系数
                BigDecimal durableUseRatio = sysConfigService.getValue(Config.Sys.Punch.耐久消耗系数);
                BigDecimal breakageUse = sysConfigService.getValue(Config.Sys.Punch.消耗破损值);
                BigDecimal defaultIncomeRatio = sysConfigService.getValue(Config.Sys.Punch.收益折算系数);
                // 查询使用中卡槽的装备
                List<PunchUserSlot> slotList = punchUserSlotService.selectByUserId(userId, 1);
                List<UserPunchSlotVo> slotVoList = new ArrayList<>();
                if (!slotList.isEmpty()) {
                    // 耐久度削弱比列
                    List<PunchWeakenConfig> weakenConfigList = punchWeakenConfigService.lambdaQuery()
                            .orderByAsc(PunchWeakenConfig::getDurabilityRate)
                            .list();
                    // 将使用中的卡槽按照nftNumberId分组一个装备id对应一个卡槽
                    Map<Long, PunchUserSlot> nftNumberIdSlotIdMap = slotList.stream().collect(Collectors.toMap(PunchUserSlot::getNftNumberId, slot -> slot));
                    // 根据卡槽中的装备id查询用户装备信息
                    List<UserPunchEquipmentVo> equipmentVoList = baseMapper.equipmentList(nftNumberIdSlotIdMap.keySet());

                    slotVoList = equipmentVoList.stream().map(equipment -> {
                        UserPunchSlotVo slotVo = new UserPunchSlotVo();
                        PunchUserSlot slot = nftNumberIdSlotIdMap.get(equipment.getNftNumberId());
                        slotVo.setSlotId(slot.getSlotId());
                        slotVo.setEquipment(equipment);
                        slotVo.setExpireTime(slot.getExpireTime());
                        // 判断卡槽状态是否超时 如果卡槽过期时间不为空并且当前时间大于过期时间，则设置卡槽状态为过期
                        if (Objects.nonNull(slot.getExpireTime()) && LocalDateTime.now().isAfter(slot.getExpireTime())) {
                            slotVo.setStatus(2); // 失效
                        } else {
                            slotVo.setStatus(slot.getStatus());
                        }
                        // 设置槽装备 当天已打卡次数和当天最大可打卡次数
                        slotVo.setTodayPunchCount(slot.getTodayPunchCount());
                        slotVo.setTodayPunchTotalCount(rule.getPunchCount());
                        // 判断装备警告状态
                        equipment.setIsInsDurability(equipment.getDurability().compareTo(durableUseRatio.multiply(BigDecimal.valueOf(equipment.getTotalAttrCount())).setScale(2, RoundingMode.DOWN)) < 0);
                        equipment.setIsBreakage(equipment.getBreakage().compareTo(breakageUse) < 0);
                        // 大于等于当前耐久度的最小耐久度限制
                        // 如：当前耐久度为80， 配置里有90、80、70， 则取80
                        PunchWeakenConfig punchWeakenConfig = weakenConfigList.stream()
                                .filter(t -> t.getDurabilityRate().compareTo(equipment.getDurability().divide(equipment.getTotalDurability(), 2, RoundingMode.UP)) >= 0)
                                .findFirst()
                                .orElse(null);
                        equipment.setNextPunchWeaken(punchWeakenConfig);
                        // 计算装备的下次空投值(计算衰减)
                        BigDecimal income = userPunchService.calcPunchIncome(BeanUtil.copyProperties(equipment, Equipment.class), defaultIncomeRatio, punchWeakenConfig, true);
                        equipment.setPunchAirdropBeans(income);
                        // 设置装备剩余空投和总空投
                        Equipment sysEqu = BeanUtil.copyProperties(equipment, Equipment.class);
                        equipment.setRemainAirdrop(userPunchService.calcPunchIncomeByWeakenV2(sysEqu, defaultIncomeRatio,  breakageUse, durableUseRatio, punchWeakenConfig, false));
                        equipment.setTotalAirdrop(userPunchService.calcPunchIncomeByWeakenV2(sysEqu, defaultIncomeRatio,  breakageUse, durableUseRatio, punchWeakenConfig, true));
                        equipment.setRemainPunch(equipment.getBreakage().divide(breakageUse, 0, RoundingMode.DOWN).intValue());
                        return slotVo;
                    }).collect(Collectors.toList());
                }
                // 查询剩余未装备的卡槽
                List<PunchUserSlot> remainSlotList = punchUserSlotService.selectInByUserId(userId, CollectionUtil.toList(0, -1));
                if (CollUtil.isNotEmpty(remainSlotList)) {
                    // 检测remainSlotList中的卡槽expireTime不会null平且是否过期 如果过期则设置状态为2
                    remainSlotList.forEach(slot -> {
                        if (Objects.nonNull(slot.getExpireTime()) && LocalDateTime.now().isAfter(slot.getExpireTime())) {
                            slot.setStatus(2);
                        }
                    });
                    slotVoList.addAll(BeanUtil.copyToList(remainSlotList, UserPunchSlotVo.class));
                    // 获取用户剩余可以添加装备的卡槽数量 = remainSlotList中status=0的数量
                    vo.setRemainSlotCount((int) remainSlotList.stream().filter(t -> t.getStatus() == 0).count());
                }
                vo.setSlotList(slotVoList);
                // 计算卡槽中所有装备相加打卡收益
                BigDecimal totalIncome = slotVoList.stream()
                        .filter(t -> t.getEquipment() != null)
                        .map(t -> t.getEquipment().getPunchAirdropBeans())
                        .reduce(BigDecimal.ZERO, BigDecimal::add);
                vo.setSlotTotalIncome(totalIncome);
                // 总卡槽数 = slotVoList的长度
                vo.setSlotTotalCount(slotVoList.size());
                // 实际有效卡槽数量 = slotVoList中status=1并且并且equipment的punchAirdripBeans>0的数量
                vo.setSlotEffectiveCount((int) slotVoList.stream()
                        .filter(t -> t.getStatus() == 1 && t.getEquipment() != null && t.getEquipment().getPunchAirdropBeans().compareTo(BigDecimal.ZERO) > 0)
                        .count());
                // 设置今日是否还可以打卡
                vo.setTodayCanPunch(LocalDateTimeUtil.isSameDay(vo.getNextPunchTime(), LocalDateTime.now()));
            }
        }

        // 获取队伍信息
        ConsumerTeamVo myTeam = consumerTeamService.getMyTeam(userId);
        ConsumerPunchTeamVo teamVo = BeanCopierUtil.copy(myTeam, ConsumerPunchTeamVo.class);
        vo.setTeamInfo(teamVo);
        if(Objects.nonNull(teamVo)){
            // 获取当前队伍的打卡收益信息
            BigDecimal teamTodayIncome = punchUserRecordService.getUserTotalIncome(userId, myTeam.getId(), LocalDate.now());
            ConsumerTeamGainRule currentGain = teamGainRuleService.getPunchCountRule(teamVo.getTotalPunch(),myTeam.getRank());
            if(currentGain != null){
                TeamGainInfoVo gainVo = new TeamGainInfoVo();
                gainVo.setGain(currentGain.getGain());
                gainVo.setLackPunchCount(0);
                gainVo.setPunchCount(currentGain.getTotalPunch());
                gainVo.setIncome(teamTodayIncome.multiply(gainVo.getGain()).setScale(2, RoundingMode.UP));
                teamVo.setCurrentGainInfo(gainVo);
            }
            // v1.6.0：打卡页更改。由于打卡页数据已写死，取的下阶段的增益百分比。现把下阶段的收益百分比改为当前阶段的。若没有达到最低打卡次数满足的百分比，就返回百分之零。
            ConsumerTeamGainRule nextGain = teamGainRuleService.getNextPunchCountRule(teamVo.getTotalPunch(),myTeam.getRank());
            if(nextGain != null){
                TeamGainInfoVo gainVo = new TeamGainInfoVo();
                gainVo.setGain(currentGain != null?currentGain.getGain():BigDecimal.ZERO);
                gainVo.setLackPunchCount(0);
                gainVo.setPunchCount(currentGain != null?currentGain.getTotalPunch():0);
                gainVo.setIncome(teamTodayIncome.multiply(gainVo.getGain()).setScale(2, RoundingMode.UP));
                teamVo.setNextGainInfo(gainVo);
            }
        }
        return vo;
    }

    /**
     * 添加装备到卡槽
     *
     * @param userId 用户ID
     * @param params 添加卡槽参数
     */
    @Transactional(rollbackFor = Exception.class)
    public void slotAdd(Long userId, UserAddSlotDto params) {
        // 参数校验：添加指定卡槽时，装备数量必须是一个
        if (Objects.nonNull(params.getSlotId()) && params.getUserNftId().size() != 1) {
            throw new ServiceException("添加指定卡槽参数错误，装备数量必须是一个");
        }

        // 获取用户打卡配置并校验是否选择打卡规则
        PunchUserConfig config = getConfig(userId);
        if (Objects.isNull(config) || Objects.isNull(config.getPunchRuleId())) {
            throw new ServiceException("暂无时间段无法打卡");
        }

        // 获取用户所有未使用的卡槽
        List<PunchUserSlot> userSlotList = punchUserSlotService.selectByUserId(userId, 0);
        if (CollUtil.isEmpty(userSlotList)) {
            throw new ServiceException("无法添加，用户卡槽已满");
        }

        // 根据用户id与userNftId获取用户所有装备并校验是否都在闲置中
        List<SysUserNft> userNftList = userNftService.list(Wrappers.lambdaQuery(SysUserNft.class)
                .in(SysUserNft::getId, params.getUserNftId())
                .eq(SysUserNft::getUserId, userId)
                .eq(SysUserNft::getNftStatus, UserNftStatusEnum.NORMAL));
        if (userNftList.size() != params.getUserNftId().size()) {
            throw new ServiceException("部分装备不存在或状态不允许添加");
        }

        // 添加指定卡槽一个装备
        if (Objects.nonNull(params.getSlotId())) {
            addEquipmentToSpecificSlot(userId, params, userSlotList, userNftList);
        } else {
            // 循环用户剩下未装备的卡槽依次将装备添加到卡槽
            addEquipmentToMultipleSlots(userId, userSlotList, userNftList);
        }
    }

    /**
     * 将装备添加到指定的卡槽
     * @param userId
     * @param params
     * @param userSlotList
     * @param userNftList
     */
    private void addEquipmentToSpecificSlot(Long userId, UserAddSlotDto params, List<PunchUserSlot> userSlotList, List<SysUserNft> userNftList) {
        PunchUserSlot slot = userSlotList.stream()
                .filter(t -> t.getSlotId().equals(params.getSlotId()))
                .findFirst()
                .orElseThrow(() -> new ServiceException("指定用户卡槽不存在"));
        // 更新卡槽状态装上装备
        punchUserSlotService.lambdaUpdate()
                .eq(PunchUserSlot::getUserId, userId)
                .eq(PunchUserSlot::getSlotId, params.getSlotId())
                .set(PunchUserSlot::getStatus, 1)
                .set(PunchUserSlot::getUserNftId, params.getUserNftId().get(0))
                .set(PunchUserSlot::getNftNumberId, userNftList.get(0).getNftNumberId())
                .update();
        // 更新装备状态
        userNftService.updateStatus(params.getUserNftId().get(0), UserNftStatusEnum.USED);
    }

    /**
     * 快捷添加到所有剩余卡槽中
     * @param userId
     * @param userSlotList
     * @param userNftList
     */
    private void addEquipmentToMultipleSlots(Long userId, List<PunchUserSlot> userSlotList, List<SysUserNft> userNftList) {
        for (int i = 0; i < Math.min(userSlotList.size(), userNftList.size()); i++) {
            PunchUserSlot slot = userSlotList.get(i);
            SysUserNft userNft = userNftList.get(i);
            // 更新卡槽状态装上装备
            punchUserSlotService.lambdaUpdate()
                    .eq(PunchUserSlot::getUserId, userId)
                    .eq(PunchUserSlot::getSlotId, slot.getSlotId())
                    .set(PunchUserSlot::getStatus, 1)
                    .set(PunchUserSlot::getUserNftId, userNft.getId())
                    .set(PunchUserSlot::getNftNumberId, userNft.getNftNumberId())
                    .update();
            // 更新装备状态
            userNftService.updateStatus(userNft.getId(), UserNftStatusEnum.USED);
        }
    }

    /**
     * 卸下装备
     * @param userId
     * @param params
     */
    public void slotWholesale(Long userId, UserAddSlotDto params) {
        // 获取用户所有使用中的卡槽
        List<PunchUserSlot> userSlotList = punchUserSlotService.selectByUserId(userId, 1);
        if (CollUtil.isEmpty(userSlotList)) {
            throw new ServiceException("无法卸下卡槽已空");
        }
        BigDecimal defaultBreakageUse = sysConfigService.getValue(Config.Sys.Punch.消耗破损值);
        BigDecimal defaultDurableRatio = sysConfigService.getValue(Config.Sys.Punch.耐久消耗系数);
        //如果是卸下指定卡槽，装备数量必须是一个
        if (!params.getIsAllUnload()) {
            if (Objects.isNull(params.getSlotId()) || params.getUserNftId().size() != 1) {
                throw new ServiceException("卸下指定卡槽参数错误");
            }
            // 查找指定的卡槽
            for (PunchUserSlot slot : userSlotList) {
                if (slot.getSlotId().equals(params.getSlotId())) {
                    // 校验卡槽中的NFT是否与指定的NFT匹配
                    if (!Objects.equals(slot.getUserNftId(), params.getUserNftId().get(0))) {
                        throw new ServiceException("指定用户卡槽装备不匹配");
                    }
                    // 如果已经打卡，则不能卸下
                    if (slot.getTodayPunchCount() > 0) {
                        // 获取装备信息
                        Equipment equipment = equipmentService.getByUserNftId(params.getUserNftId().get(0), userId);
                        boolean breakage = equipment.getBreakage().compareTo(defaultBreakageUse) > 0;
                        boolean durable = equipment.getDurability().compareTo(userPunchService.calcDurabilityUsd(equipment, defaultDurableRatio)) > 0;
                        // 虽然已经打卡但是如果当前打卡装备破损度或者耐久度不够打卡一次消耗时支持切换装备
                        if (breakage || durable) {
                            throw new ServiceException("打卡中不能调整装备");
                        }
                    }
                    // 更新卡槽和NFT的状态
                    punchUserSlotService.lambdaUpdate()
                            .eq(PunchUserSlot::getUserId, userId)
                            .eq(PunchUserSlot::getSlotId, slot.getSlotId())
                            .set(PunchUserSlot::getStatus, 0)
                            .set(PunchUserSlot::getUserNftId, null)
                            .set(PunchUserSlot::getNftNumberId, null)
                            .update();
                    userNftService.updateStatus(params.getUserNftId().get(0), UserNftStatusEnum.NORMAL);
                    return;
                }
            }
            throw new ServiceException("指定用户卡槽不存在");
        } else {
            // 如果所有卡槽都已经打卡，则不能使用一键卸下
            boolean isAllPunch = userSlotList.stream().allMatch(t -> t.getTodayPunchCount() > 0);
            if (isAllPunch) {
                throw new ServiceException("打卡中不能调整装备");
            }
            // 卸下所有卡槽
            for (PunchUserSlot slot : userSlotList) {
                if (slot.getTodayPunchCount() == 0) {
                    punchUserSlotService.lambdaUpdate()
                            .eq(PunchUserSlot::getUserId, userId)
                            .eq(PunchUserSlot::getSlotId, slot.getSlotId())
                            .set(PunchUserSlot::getStatus, 0)
                            .set(PunchUserSlot::getUserNftId, null)
                            .set(PunchUserSlot::getNftNumberId, null)
                            .update();
                    userNftService.updateStatus(slot.getUserNftId(), UserNftStatusEnum.NORMAL);
                }
            }
        }
    }

    /**
     * 切换卡槽装备
     * @param userId
     * @param params
     */
    @Transactional(rollbackFor = Exception.class)
    public void slotHandoff(Long userId, EditUserPunchConfigDto params) {
        // 获取用户指定的卡槽
        PunchUserSlot userSlot = punchUserSlotService.selectSlotByUserIdAndSlotId(userId, params.getSlotId());
        if (Objects.isNull(userSlot)) {
            throw new ServiceException("用户卡槽不存在");
        }
        UserAddSlotDto dto = new UserAddSlotDto();
        // 如果指定的userNftId与卡槽中的userNftId相同或者则执行卸下操作
        if (Objects.equals(userSlot.getUserNftId(), params.getUserNftId())) {
            dto.setSlotId(params.getSlotId());
            dto.setIsAllUnload(false);
            dto.setUserNftId(Collections.singletonList(params.getUserNftId()));
            // 卸下
            slotWholesale(userId, dto);
        } else {
            // 如果指定的userNftId与卡槽中的userNftId不相同则执行切换操作 先卸下原卡槽中的装备再添加新的装备
            dto.setSlotId(params.getSlotId());
            dto.setIsAllUnload(false);
            dto.setUserNftId(Collections.singletonList(userSlot.getUserNftId()));
            // 卸下
            slotWholesale(userId, dto);
            dto.setUserNftId(Collections.singletonList(params.getUserNftId()));
            // 添加
            slotAdd(userId, dto);
        }
    }
}
